/*
 * Copyright (c) 2008 Openviewing
 *
 * Permission is hereby granted, free of charge, to any person
 * obtaining a copy of this software and associated documentation
 * files (the "Software"), to deal in the Software without
 * restriction, including without limitation the rights to use,
 * copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the
 * Software is furnished to do so, subject to the following
 * conditions:
 *
 * The above copyright notice and this permission notice shall be
 * included in all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
 * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
 * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
 * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
 * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
 * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
 * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
 * OTHER DEALINGS IN THE SOFTWARE.
 *
 * Created: 09-Sep-2006 15:14:17
 */
package org.openviewing.foundation.control.factory.template;

import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import java.util.Map.Entry;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.openviewing.foundation.control.factory.BeanException;
import org.openviewing.foundation.control.factory.BeanRegistryStrategy;

/** Template Class.
 *
 *  This class follows the 'template' and 'factory method' design patterns [GOF] to provide the ability to create java beans and aslso provides inversion control [RJ].
 *
 *  References
 *  [GOF]
 *  Gamma, Erich. Helm, Richard. Johnson, Ralph. Vlissides, Johhn. <i>Design Patterns, Elements of reuable Object-Oriented Software</i> Addison-Wesley, 1995.
 *  [RJ]
 *  Johnson, Rod. <i>J2EE Design and Development</i> Wiley Publishing Inc, 2003.
 */
public abstract class BeanFactoryTemplate implements BeanRegistryStrategy
{
    // Tempalte attributes /////////////////////////////////////////////////////
    /** Container holding the bean metadata. */
    protected final HashMap<String, String> beanDefinitions    = new HashMap<String, String>();
    /** Invoking class Logger*/
    protected final Log                     log                = LogFactory.getLog(this.getClass());

    // Private operations //////////////////////////////////////////////////////
    @SuppressWarnings("unchecked")
    private Object innerClassCreate(final Class clazz, final Object[] args)
    {
        Object   instance = null;
        Object[] data     = null;
        if(instance == null && clazz.getName().contains("$"))
        {
            try
            {
                String parent = clazz.getName().substring(0, clazz.getName().indexOf('$'));
                if(args == null)
                {
                    data = new Object[] { Class.forName(parent).newInstance() };
                }
                else
                {
                    List<Object> temp = new ArrayList<Object>();
                    temp.add(Class.forName(parent).newInstance());
                    for(int count = 0; count < args.length; count++)
                    {
                        temp.add(args[0]);
                    }
                    data = temp.toArray();
                }
                Class[] types = getTypes(data);
                instance = this.assignableCreate(clazz, types, data);
            }
            catch(InstantiationException e)
            {
                throw new BeanException(String.format("Unable to instantiate %s class", clazz.getName()), e);
            }
            catch(IllegalAccessException e)
            {
                throw new BeanException(String.format("Illegal access of %s class", clazz.getName()), e);
            }
            catch(ClassNotFoundException e)
            {
                throw new BeanException(String.format("%s class was not found", clazz.getName()), e);
            }
        }
        return instance;
    }
    @SuppressWarnings("unchecked")
    private Class[] getTypes(final Object[] args)
    {
        Class[] types = new Class[args.length];
        for(int i = 0; i < args.length; i++)
        {
            this.log.debug(String.format("Argument %d is a %s", i, args[i] == null ? "null" : args[i].getClass().getName()));
            types[i] = args[i].getClass();
        }
        return types;
    }
    /** Flyweight creator method.*/
    @SuppressWarnings("unchecked")
    private Object assignableCreate(Class clazz, Class[] types, Object[] data)
    {
        Object        instance      = null;
        Constructor[] clist         = clazz.getDeclaredConstructors();
        boolean       tryInvocation = true;

        for(Constructor c : clist)
        {
            Class[] cpt = c.getParameterTypes();
            
            if(cpt.length == types.length)
            {
                for(int j = 0; j < cpt.length; j++)
                {
                    if(cpt[j].isAssignableFrom(types[j]))
                    {
                        continue;
                    }
                    else
                    {
                        tryInvocation = false;
                        break;
                    }
                }
                try
                {
                    if(tryInvocation)
                    {
                        instance = (Object) c.newInstance(data);
                    }
                    else
                    {
                        tryInvocation = true;
                    }
                }
                catch (IllegalArgumentException e)
                {
                    throw new BeanException(String.format("Illegal access of %s class", clazz.getName()), e);
                }
                catch (InstantiationException e)
                {
                    throw new BeanException(String.format("Unable to instantiate %s class", clazz.getName()), e);
                }
                catch (IllegalAccessException e)
                {
                    throw new BeanException(String.format("Illegal access of %s class", clazz.getName()), e);
                }
                catch (InvocationTargetException e)
                {
                    throw new BeanException(String.format("Unable to invocate %s class", clazz.getName()), e);
                }
            }
        }
        return instance;
    }
    /** Factory Method
     *
     *  This operation creates the bean.
     *  @param    name    The name of the class to be instantiated.
     *  @param    type    The type of class expected.
     *  @param    args    The constructor arguments in the correct order as the signature.
     *  @return    Instance of the object.
     */
    @SuppressWarnings("unchecked")
    private Object create(final String name, final Class type, final Object[] args)
    {
        Class       clazz       = null;
        Class[]     types       = null;
        Constructor constructor = null;
        Object      instance    = null;

        this.log.debug(String.format("Parameters:\nName: %s\nType: %s\nArgs: %s", name, type, (args == null ? "" : Arrays.toString(args))));

        try
        {
            // Get the Class based on the name argument.
            clazz = Class.forName(name);
            // Default constructor?
            if(args != null && args.length > 0)
            { // No
                this.log.debug("Got arguments");
                types = getTypes(args);
                this.log.debug(String.format("Using parameter constructor %s", Arrays.toString(types)));
                constructor    = clazz.getConstructor(types);
                instance = (Object) constructor.newInstance(args);
            }
            else
            { // Yes
                this.log.debug("Using default constructor");
                instance = (Object) clazz.newInstance();
            }
        }
        catch (ClassNotFoundException e)
        {
            throw new BeanException(String.format("%s class was not found", type), e);
        }
        catch (InstantiationException e)
        {
            instance = this.innerClassCreate(clazz, args);
        }
        catch (InvocationTargetException e)
        {
            throw new BeanException(String.format("Unable to invocate %s class", type), e);
        }
        catch (IllegalAccessException e)
        {
            throw new BeanException(String.format("Illegal access of %s class", type), e);
        }
        catch (NoSuchMethodException e)
        {
            this.log.debug(String.format("No such method for %s class so seeing if types are assignable.", name));
            // Try and find the constructor again
            instance = this.assignableCreate(clazz, types, args);
            if(instance == null)
            {
                instance = this.innerClassCreate(clazz, args);
            }
        }
        // Check that the class type is the one asked for or can be assignable to it.
        if(type != clazz && type.isAssignableFrom(clazz) == false)
        {
            throw new BeanException(String.format("Expected %s class but found %s", type.getName(), clazz.getName()));
        }
        if(instance == null)
        {
            StringBuffer    msg            = null;
            msg = new StringBuffer("Constructor details:\n");
            for(int i = 0; i < clazz.getDeclaredConstructors().length; i++)
            {
                Constructor ct = clazz.getDeclaredConstructors()[i];
                msg.append("name = ");
                msg.append(ct.getName());
                msg.append("\n");
                msg.append("decl class = ");
                msg.append(ct.getDeclaringClass());
                msg.append("\n");
                Class[] pvec = ct.getParameterTypes();
                for (int j = 0; j < pvec.length; j++)
                {
                    msg.append("param #");
                    msg.append(j);
                    msg.append(" ");
                    msg.append(pvec[j]);
                    msg.append("\n");
                }
                Class[] evec = ct.getExceptionTypes();
                for (int j = 0; j < evec.length; j++)
                {
                    msg.append("exc #");
                    msg.append(j);
                    msg.append(" ");
                    msg.append(evec[j]);
                    msg.append("\n");
                }
                msg.append("-----\n");
            }
            throw new BeanException(String.format("Unable to instantiate %s class as arguments %s do not match constructors found, %s", name, Arrays.toString(types), msg.toString()));
        }
        return instance;
    }
    // Public operations ///////////////////////////////////////////////////////
    /** Bean accessor
     *
     *  Retrieve the bean with the given name.
     *
     *  @param    name    The logical name of the bean to be retrieved.
     *  @return    The generated bean as defined by its configuration.
     *  @see    org.arcanenexus.foundation.factory.BeanRegistryStrategy#getBean(String) Bean Factory
     */
    public final Object getBean(final String name)
    {
        String        className    = null;
        Object        instance    = null;

        try
        {
            if(this.beanDefinitions.containsKey(name))
            {
                className = this.beanDefinitions.get(name);
                instance = this.create(className, Class.forName(className), null);
            }
            else
            {
                throw new BeanException(String.format("%s bean has not registered", name));
            }
        }
        catch (ClassNotFoundException e)
        {
            throw new BeanException(String.format("%s class was not created because: %s", name, e.getMessage()), e);
        }

        return instance;
    }

    /** Bean accessor
     *
     *  Retrieves the bean with the given name and class.
     *
     *  @param    name        The logical name of the bean to be retrieved.
     *  @param    type    The required class type to be retrieved.
     *  @return    The generated bean as defined by its configuration.
     *  @see    org.arcanenexus.foundation.factory.BeanRegistryStrategy#getBean(String, Class) Bean Factory
     */
    @SuppressWarnings("unchecked")
    public final Object getBean(final String name, final Class type)
    {
        String        className    = null;
        Object        instance    = null;

        if(this.beanDefinitions.containsKey(name))
        {
            className = this.beanDefinitions.get(name);
            instance = this.create(className, type, null);
        }
        else
        {
            throw new BeanException(String.format("%s bean is not registered", name));
        }

        return instance;
    }

    /** Bean accessor
     *
     *  Retrieves the bean of the given name using the constructor matching args.
     *
     *  @param    name    The logical name of the bean to be retrieved.
     *  @param    args    The arguments passed to the constructor.
     *  @return    The generated bean as defined by its configuration.
     *  @see    org.arcanenexus.foundation.factory.BeanRegistryStrategy#getBean(String, Object[]) Bean Factory
     */
    public final Object getBean(final String name, final Object[] args)
    {
        String        className    = null;
        Object        instance    = null;

        try
        {
            if(this.beanDefinitions.containsKey(name))
            {
                className = this.beanDefinitions.get(name);
                instance = this.create(className, Class.forName(className), args);
            }
            else
            {
                throw new BeanException(String.format("%s bean is not registered", name));
            }
        }
        catch (ClassNotFoundException e)
        {
            throw new BeanException(String.format("%s class was not found", name), e);
        }

        return instance;
    }

    /** Bean accessor
     *
     *  Retrieves the bean with the given name and class using the constructor matching args.
     *
     *  @param    name    The logical name of the bean to be retrieved.
     *  @param    type    The required class type to be retrieved.
     *  @param    args    The arguments passed to the constructor.
     *  @return    The generated bean as defined by its configuration.
     *  @see    org.arcanenexus.foundation.factory.BeanRegistryStrategy#getBean(String, Class, Object[]) Bean Factory
     */
    @SuppressWarnings("unchecked")
    public final Object getBean(final String name, final Class type, final Object[] args)
    {
        String        className    = null;
        Object        instance    = null;

        if(this.beanDefinitions.containsKey(name))
        {
            className = this.beanDefinitions.get(name);
            instance = this.create(className, type, args);
        }
        else
        {
            throw new BeanException(String.format("%s bean is not registered", name));
        }

        return instance;
    }

    /**
     * @see org.arcanenexus.foundation.factory.BeanRegistryStrategy#getBean(java.lang.String, java.util.Map)
     *
     * @param   name        The reference name of the bean to be instantiated.
     * @param   attributes  Constructor attributes to be used.
     * @return  An instantiated bean.
     */
    @SuppressWarnings("unchecked")
    public final Object getBean(final String name, final Map<String, Object> attributes)
    {
        String                className    = null;
        Class                clazz        = null;
        Object                instance    = null;
        Set                    keys        = null;
        Iterator            keysIt        = null;
        Object                value        = null;
        Class[]                paramType    = new Class[1];
        String                methodName    = null;
        Object[]                args        = new Object[1];

        try
        {
            if(this.beanDefinitions.containsKey(name))
            {
                className = this.beanDefinitions.get(name);
                clazz = Class.forName(className);
                instance = this.create(className, clazz, null);
                if((attributes != null) && (attributes.isEmpty() == false))
                {
                    keys = attributes.entrySet();
                    keysIt = keys.iterator();
                    while (keysIt.hasNext())
                    {
                        Entry<String, Object> attribute = (Entry<String, Object>) keysIt.next();
                        value = attribute.getValue();
                        log.debug(String.format("%s=(%s)%s", attribute.getKey(), value.getClass().getName(), value.toString()));
                        // determine value type
                        paramType[0] = value.getClass();
                        args[0] = value;
                        // Set the attribute value.
                        try
                        {
                            methodName = String.format("set%c%s", attribute.getKey().toUpperCase(Locale.getDefault()).charAt(0), attribute.getKey().substring(1));
                            log.debug(String.format("Trying to execute %s(%s) method for %s bean", methodName, Arrays.toString(args), name));
                            Method method = clazz.getMethod(methodName, paramType);
                            method.invoke(instance, args);
                        }
                        catch (SecurityException e)
                        {
                            throw new BeanException(String.format("Access denied to %s method for %s bean", methodName, name), e);
                        }
                        catch (NoSuchMethodException e)
                        {
                            throw new BeanException(String.format("No such %s(%s) method for %s bean", methodName, args[0].getClass(), name), e);
                        }
                        catch (IllegalAccessException e)
                        {
                            throw new BeanException(String.format("Illegal Access to %s method for %s bean", methodName, name), e);
                        }
                        catch (InvocationTargetException e)
                        {
                            throw new BeanException(String.format("Invocation Target %s method not found for %s bean", methodName, name), e);
                        }
                    }
                }
            }
            else
            {
                throw new BeanException(String.format("%s bean is not registered", name));
            }
        }
        catch (ClassNotFoundException e)
        {
            throw new BeanException(String.format("%s class was not found", name), e);
        }

        return instance;
    }

    // Constructors ////////////////////////////////////////////////////////////
    /** Default constructor. */
    public BeanFactoryTemplate()
    {
        super();
    }
}
